package org.mzy.basic.service.impl;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.mzy.basic.config.BussinessException;
import org.mzy.basic.constant.BaseConstants;
import org.mzy.basic.dto.AccountLoginDto;
import org.mzy.basic.dto.AccountLoginDto2;
import org.mzy.basic.dto.WechatLoginDto;
import org.mzy.basic.jwt.JwtUtils;
import org.mzy.basic.jwt.RsaUtils;
import org.mzy.basic.jwt.UserInfo;
import org.mzy.basic.service.ILoginService;
import org.mzy.basic.util.AjaxResult;
import org.mzy.basic.util.HttpUtil;
import org.mzy.basic.util.MD5Utils;
import org.mzy.basic.util.StrUtils;
import org.mzy.system.domain.Menu;
import org.mzy.system.mapper.MenuMapper;
import org.mzy.system.mapper.PermissionMapper;
import org.mzy.user.domain.Logininfo;
import org.mzy.user.domain.User;
import org.mzy.user.domain.Wxuser;
import org.mzy.user.mapper.LogininfoMapper;
import org.mzy.user.mapper.UserMapper;
import org.mzy.user.mapper.WxuserMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.security.PrivateKey;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @Package: org.mzy.basic.service.impl
 * @ClassName: LoginServiceImpl
 * @Date:2022/10/21 16:06
 * @Description:
 */
@Service
public class LoginServiceImpl implements ILoginService {
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private LogininfoMapper logininfoMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private WxuserMapper wxuserMapper;
    @Autowired
    private PermissionMapper permissionMapper;
    @Autowired
    private MenuMapper menuMapper;
    @Value("${jwt.rsa.pri}")
    private String jwtRsaPrivate;

    /**
     * @Title: loginSuccessJwtHandler
     * @Description: 用户登录成功后生成JWT加密串并放入到map对象中
     * @Author: Mr.She
     * @Version: 1.0
     * @Date: 2022/6/30 13:23
     * @Parameters: [cn.itsource.user.domain.Logininfo]
     * @Return java.util.Map<java.lang.String, java.lang.Object>
     */
    public HashMap<String, Object> loginSuccessJwtHandler(Logininfo logininfo) {
        // 1.创建UserInfo对象，存储logininfo对象，并用于后续赋值
        UserInfo userInfo = new UserInfo();
        userInfo.setLogininfo(logininfo);
        // 2.判断是后台用户登录才需要设置菜单信息、权限信息，类型为0那么就是后台用户登录
        if (logininfo.getType() == 0) {
            // 2.1.获取用户权限信息，我们只需要sn权限编码即可
            List<String> permissions = permissionMapper.findByLogininfoId(logininfo.getId());
            // 2.2.获取用户菜单信息
            List<Menu> menus = menuMapper.findByLogininfoId(logininfo.getId());
            // 2.3.将菜单信息，权限信息放入到userinfo对象中
            userInfo.setPermissions(permissions);
            userInfo.setMenus(menus);
        }
        // 3.根据密钥文件名称获取到密钥
        PrivateKey privateKey = RsaUtils.getPrivateKey(this.getClass().getClassLoader().getResource(jwtRsaPrivate).getFile());
        // 4.根据密钥、用户信息、获取JWT加密串
        String token = JwtUtils.generateTokenExpireInMinutes(userInfo, privateKey, 30);
        // 5.按照正常逻辑我们直接返回token给前端即可，但是那样前端需要解密，我们使用简化版本JWT，还是返回用户信息
        // 封装token、用户信息、权限信息、菜单信息给前端
        HashMap<String, Object> map = new HashMap<>();
        map.put("token", token);
        map.put("logininfo", logininfo);
        map.put("permissions", userInfo.getPermissions());
        map.put("menus", userInfo.getMenus());
        return map;
    }

    /*账号登录*/
    @Override
    public Map<String, Object> accountLogin(AccountLoginDto dto) {
        // 非空校验，在dto类中已经判断
        // 判断账号是否存在 loginifo,调用根据用户名查询，传入前端传入的用户名和type
        Logininfo logininfo = logininfoMapper.findByUsername(dto.getUsername(), dto.getType());
        // 如果对象为空就异常
        if (logininfo == null) {
            throw new BussinessException("账号或密码错误");
        }
        // 判断密码是否正确，MD5Utils.encrypByMd5对密码加密，传入盐值拼接密码，得到加密密码
        String encrypPassword = MD5Utils.encrypByMd5(logininfo.getSalt() + dto.getPassword());
        // 如果加密密码为空或者和数据库的密码不匹配，就异常
        if (StringUtils.isBlank(encrypPassword) || !encrypPassword.equals(logininfo.getPassword())) {
            throw new BussinessException("账号或密码错误");
        }
        // 调用获取map的方法并返回
        return this.loginSuccessJwtHandler(logininfo);
    }

    // 手机验证码登录
    @Override
    public Map<String, Object> phonnCodeLogin(AccountLoginDto2 dto) {
        // 非空校验，在dto类中已经判断
        // 判断账号是否存在 loginifo,调用根据用户名查询，传入前端传入的用户名和type
        Logininfo logininfo = logininfoMapper.findByPhone(dto.getPhone(), dto.getType());
        // 如果对象为空就异常
        if (logininfo == null) {
            throw new BussinessException("账号或验证码错误");
        }
        // 调用UUID.randomUUID()随机生成一个uuid保存为token
        String token = UUID.randomUUID().toString();
        // 将token和logininfo添加到redis中，30分钟有效期
        redisTemplate.opsForValue().set(token, logininfo, 1440, TimeUnit.MINUTES);
        // 将logininfo敏感字段置空
        logininfo.setPassword("");
        logininfo.setSalt("");
        logininfo.setPhone("");
        // 返回给前端
        return new HashMap<String, Object>() {{
            put("token", token);
            put("logininfo", logininfo);
        }};
    }

    // 微信登录
    @Override
    public AjaxResult wechatLogin(WechatLoginDto wechatLoginDto) {
        // 非空校验
        if (StringUtils.isBlank(wechatLoginDto.getCode())) {
            throw new BussinessException("参数不能为空");
        }
        // 通过code获取token和openid
        Pair<String, String> pair = this.getOpenidAndToken(wechatLoginDto.getCode());
        // 通过poenid查询微信用户表是否有数据
        Wxuser wxuser = wxuserMapper.findByOpenid(pair.getRight());
        // 如果这个对象不为空，或者这个对象的userid不为空，就直接登录,并且返给前端token和logininfo
        if (wxuser != null && wxuser.getUserId() != null) {
            Logininfo logininfo = logininfoMapper.findByUserId(wxuser.getUserId());
            // 调用获取map的方法，传入logininfo对象，得到map集合,并返回
            Map<String, Object> map = this.getMap(logininfo);
            return AjaxResult.me().success(map);
        }
        // 如果这个对象为空，就跳转到绑定页面
        // new一个StringBuffer字符串，拼接”?accessToken=“+”返给前端的token值“+”&openId=“+”openid的值“
        StringBuffer stringBuffer = new StringBuffer().append("?accessToken=").append(pair.getLeft()).append("&openId=")
                .append(pair.getRight());
        // 返回失败语句，打印nobinder提示语句，并将resultObj赋值为刚刚生成的stringBuffer
        return AjaxResult.me().fail("nobinder").setResultObj(stringBuffer);
    }

    // 微信绑定
    @Override
    public Map<String, Object> wechatBinder(WechatLoginDto dto) {
        // 非空校验 todo
        System.out.println(dto);
        // 校验验证码是否正确
        this.verifSmsCode(dto);
        // 调用微信接口，获取用户信息
        String url = String.format(BaseConstants.WechatCodeConstant.USER_INFO, dto.getAccessToken(), dto.getOpenId());
        // 发起请求
        String str = HttpUtil.httpGet(url);
        // 将json转换成实体
        Wxuser wxuser = JSONObject.parseObject(str, Wxuser.class);
        // 通过传入的手机号找到user
        User user = userMapper.findByPhone(dto.getPhone());
        // new一个Logininfo对象
        Logininfo logininfo = new Logininfo();
        // 如果user为空
        if (null == user) {
            // 初始化user的方法
            user = this.inituser(dto);
            // 将User和Logininfo相同的字段内容复制到Logininfo中的方法
            logininfo = this.initLogininfo(user);
            // 保存logininfo
            logininfoMapper.add(logininfo);
            // 将logininfo的id赋值给user的logininfoId
            user.setLogininfoId(logininfo.getId());
            // 保存user
            userMapper.add(user);
        } else {
            // 传入user的id，通过userid找到logininfo
            logininfo = logininfoMapper.findByUserId(user.getId());
        }
        // 获取user的id，赋值给wxuser的userid
        wxuser.setUserId(user.getId());
        wxuserMapper.add(wxuser);
        // 调用获取map的方法,保存到map中并返回
        return this.getMap(logininfo);
    }

    // 通过code获取token和openid方法
    private Pair<String, String> getOpenidAndToken(String code) {
        // 将微信开放平台所需的参数添加到String
        String tokenUrl = String.format(BaseConstants.WechatCodeConstant.TOKEN_URL,
                BaseConstants.WechatCodeConstant.APPID, BaseConstants.WechatCodeConstant.SECRET, code);
        // 调用方法，发起请求
        String s = HttpUtil.httpGet(tokenUrl);
        // 将json字符串转换为json对象
        JSONObject jsonObject = JSONObject.parseObject(s);
        // 根据json中的key，获取他们对应的值
        String accessToken = jsonObject.getString("access_token");
        String openid = jsonObject.getString("openid");
        // 将accessToken和openid存到Pair中并返回
        return Pair.of(accessToken, openid);
    }

    // 获取map的方法
    private Map<String, Object> getMap(Logininfo logininfo) {
        // 使用jwt方案
        PrivateKey privateKey = RsaUtils.getPrivateKey(JwtUtils.class.getClassLoader().getResource(jwtRsaPrivate)
                .getFile());
        Map<String, Object> hashMap = new HashMap<String, Object>();
        UserInfo user = new UserInfo();
        user.setLogininfo(logininfo);
        if (0 == logininfo.getType()) {
            // 如果是后端 那么我们需要将权限放到token中
            // 菜单权限
            List<String> permissions = permissionMapper.findByLogininfoId(logininfo.getId());
            // 按钮权限
            List<Menu> menus = menuMapper.findByLogininfoId(logininfo.getId());
            user.setPermissions(permissions);
            user.setMenus(menus);
            hashMap.put("permissions", permissions);
            hashMap.put("menus", menus);
        }
        String token = JwtUtils.generateTokenExpireInMinutes(user, privateKey, 30);
        // 将logininfo敏感字段置空
        logininfo.setPassword("");
        logininfo.setSalt("");
        logininfo.setPhone("");
        // 返回给前端
        hashMap.put("token", token);
        hashMap.put("logininfo", logininfo);
        return hashMap;
    }

    // 校验验证码是否正确的方法
    private void verifSmsCode(WechatLoginDto dto) {
        // redis中 key：register:smscode:phone  value：验证码:时间戳
        // 获取到redis
        String redisSmsCode = (String) redisTemplate.opsForValue().get(String.format(BaseConstants.VerfityCodeCons
                .BINDER_SMSCODE_PHONE, dto.getPhone()));
        // 如果这个验证码为空，就抛异常
        if (StringUtils.isBlank(redisSmsCode)) {
            throw new BussinessException("验证码错误");
        }
        // 将验证码存到smsCode
        String smsCode = redisSmsCode.split(":")[0];
        // 如果验证码和redis中验证码的值不同，则抛异常
        if (!smsCode.equals(dto.getVerifyCode())) {
            throw new BussinessException("验证码错误");
        }
    }

    // 初始化user的方法
    private User inituser(WechatLoginDto dto) {
        // 生成盐值
        String salt = StrUtils.getComplexRandomString(32);
        // 设置密码
        String password = salt + "1";
        String encryPwd = MD5Utils.encrypByMd5(password);
        // 赋值并返回
        return User
                .builder()
                .username(dto.getPhone())
                .phone(dto.getPhone())
                .salt(salt)
                .password(encryPwd)
                .state(1)
                .createtime(new Date())
                .build();
    }

    // 将User和Logininfo相同的字段内容复制到Logininfo中的方法
    private Logininfo initLogininfo(User user) {
        // 声明一个Logininfo对象logininfo
        Logininfo logininfo = new Logininfo();
        // 将user中的相同字段内容复制到logininfo中
        BeanUtils.copyProperties(user, logininfo);
        // 将logininfo的type初始化为1，返回这个logininfo对象
        logininfo.setType(1);
        return logininfo;
    }
}
